import {
    WebSocketGateway,
    WebSocketServer,
    OnGatewayConnection,
    OnGatewayDisconnect,
    SubscribeMessage,
    MessageBody,
    WsResponse,
} from "@nestjs/websockets";
import { Socket, Server } from "socket.io";
import { Inject, Logger, UseFilters, UseGuards, UsePipes, ClassSerializerInterceptor } from "@nestjs/common";
import { MyWsExceptionFilter } from "src/FGIP/filter/ws-exception.filter";
import { WsEvent } from "./ws.event.enum";
import { JwtService } from "src/jwt/jwt.service";
import { OnEvent } from "@nestjs/event-emitter";
import { Emitter, ROOT_USER_ID, getAuthTokenKey } from "src/constant";
import { BroadcastService } from "./service/broadcast.service";
import { ConfigService } from "@nestjs/config";
import { CACHE_MANAGER, Cache } from "@nestjs/cache-manager";
import { MyResponse, MyWsErrorResponse, UserInfo } from "src/common/model";
import { WsCommonException } from "src/exception";
import { RolesGuard } from "src/FGIP/guard/role.guard";
import { ValidationWsMessagePipe } from "src/FGIP/pipe/validation.ws.message.pipe";
import { RequsetUser, UsePermGuard, definePermission } from "src/decorator";
import { Perm as AdminPerm } from "src/user/controller/admin.controller";
import { Perm as RolePerm } from "src/role/controller/role.controller";

import { StoreKey, StoreService } from "src/store/store.service";
import { UserService } from "src/user/service/user.service";
import { RoleEntity } from "src/entity";

export interface MySocket extends Socket {
    user: RequsetUser;
}

const Perm = definePermission("ws", {
    KICK: {
        code: "user:kick",
        name: "下线",
        description: "踢人下线",
    },
} as const);

@UseFilters(MyWsExceptionFilter)
@UseGuards(RolesGuard)
@UsePipes(
    new ValidationWsMessagePipe({
        transform: true,
        whitelist: true,
        transformOptions: { enableImplicitConversion: true },
        stopAtFirstError: true,
    }),
)
@WebSocketGateway()
export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect {
    @WebSocketServer()
    private readonly server: Server;
    private readonly logger = new Logger(SocketGateway.name);

    private userIDClientIdMap = new Map<number, string[]>();

    constructor(
        @Inject(CACHE_MANAGER) private cacheManager: Cache,
        private readonly configService: ConfigService,
        private readonly storeService: StoreService,
        private readonly userService: UserService,
        private readonly jwtService: JwtService,
        private readonly broadcastService: BroadcastService,
    ) {}

    private getAuthToken(client: MySocket) {
        return client.handshake.headers.authorization.slice(7);
    }

    private async verifyUser(token: string) {
        return await this.jwtService.verifyUser(token);
    }

    async handleConnection(client: MySocket) {
        // 不优雅,但有效的处理方式
        if (!this.broadcastService.server) this.broadcastService.server = this.server;
        try {
            const token = this.getAuthToken(client);
            client.user = await this.verifyUser(token);

            if (!this.storeService.has(StoreKey.ONLINE_USER_IDS)) {
                this.storeService.set(StoreKey.ONLINE_USER_IDS, new Set([client.user.id]));
            } else {
                this.storeService.get(StoreKey.ONLINE_USER_IDS).add(client.user.id);
            }

            this.logger.log(`user connection userid: ${client.user.id} , clientid: ${client.id}`);

            if (!this.configService.get<boolean>("MULTI_DEVICE_LOGIN")) {
                if (this.userIDClientIdMap.has(client.user.id)) {
                    const clientID = this.userIDClientIdMap.get(client.user.id);
                    const cacheToken = await this.cacheManager.get<string>(getAuthTokenKey(client.user.id));

                    if (!cacheToken) await this.cacheManager.set(getAuthTokenKey(client.user.id), token);

                    if (token == cacheToken) {
                        clientID.push(client.id);
                        this.userIDClientIdMap.set(client.user.id, clientID);
                    } else {
                        throw new Error("您的账号已在其他地方登录");
                    }
                } else {
                    this.userIDClientIdMap.set(client.user.id, [client.id]);
                }
            }
            this.broadcastService.updateUserOnlineStatus();
        } catch (error) {
            client.emit(WsEvent.EXCEPTION, new MyWsErrorResponse(401, "", `验证错误: ${error.message}`));
            client.disconnect();
        }
    }

    handleDisconnect(client: MySocket) {
        if (this.storeService.has(StoreKey.ONLINE_USER_IDS)) {
            this.storeService.get(StoreKey.ONLINE_USER_IDS).delete(client.user.id);
        }
        if (client.user && this.userIDClientIdMap.has(client.user.id)) {
            const clientID = this.userIDClientIdMap.get(client.user.id);
            const index = clientID.indexOf(client.id);
            if (index > -1) {
                clientID.splice(index, 1);
                this.userIDClientIdMap.set(client.user.id, clientID);
            }
        }
        this.broadcastService.updateUserOnlineStatus();
        this.logger.log(`user disconnect userid: ${client?.user?.id} , clientid: ${client.id}`);
    }

    @UsePermGuard(Perm.KICK)
    @SubscribeMessage(WsEvent.BACKGROUND_KICK_USER)
    async kickUser(@RequsetUser() user: RequsetUser, @MessageBody() kickIds: number[]) {
        kickIds.forEach((id) => {
            if (![user.id, ROOT_USER_ID].includes(id)) {
                this.closeConnectionByUserId(id, "", new WsCommonException(777, `被管理员: ${user.username} 扫地出门`));
            }
        });
    }

    @UsePermGuard(AdminPerm.QUERY)
    @SubscribeMessage(WsEvent.BROADCAST_ONLINE_USER)
    onlineUser() {
        this.broadcastService.updateUserOnlineStatus();
    }

    @OnEvent(Emitter.CLOSE_WS_BY_USER_ID)
    async closeConnectionByUserId(id: number, newToken: string, error = new WsCommonException(404, "连接中断")) {
        if (this.userIDClientIdMap.has(id)) {
            const sockets = this.server.of("").sockets;
            this.userIDClientIdMap.get(id).forEach((sid) => {
                const client = sockets.get(sid) as MySocket;
                if (client && this.getAuthToken(client) != newToken) {
                    client.emit(WsEvent.EXCEPTION, new MyResponse(error.code, null, error.message).toJson());
                    client.disconnect();
                }
            });
        }
    }

    @OnEvent(Emitter.UPDATE_STATUS)
    async updateStatus(type: "update.user" | "remove.user" | "update.role" | "remove.role", ...data: number[] | RoleEntity[]) {
        const sockets = this.server.of("").sockets;

        let users;

        sockets.forEach(async (client: MySocket) => {
            if (client.disconnected) return;

            if (is<number[]>(data, () => type == "update.user") && data.includes(client.user.id)) {
                client.user = await this.verifyUser(this.getAuthToken(client));
                client.emit(WsEvent.REFRESH_USER_STATUS);
            }

            if (
                (["update.user", "remove.user"].includes(type) && RolesGuard.verifyUser(client.user, [AdminPerm.QUERY])) ||
                (["update.role", "remove.role"].includes(type) &&
                    RolesGuard.verifyUser(client.user, [RolePerm.QUERY, AdminPerm.ADD, AdminPerm.UPDATE]))
            ) {
                if (!users && is<number[]>(data, () => type == "update.user") && data.length > 0) {
                    users = await this.userService.queryUserByIds(...data);
                }
                client.emit(WsEvent.BROADCAST_USER_STATUS, {
                    type,
                    data: users || data,
                });
            }
        });
    }
}
